﻿//"The contents of this file are subject to the Mozilla Public License
//Version 1.1 (the "License"); you may not use this file except in
//compliance with the License. You may obtain a copy of the License at
//http://www.mozilla.org/MPL/

//Software distributed under the License is distributed on an "AS IS"
//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
//License for the specific language governing rights and limitations
//under the License.

//The Original Code is TSharp.

//The Initial Developer of the Original Code is Mar3ek (http://mar3ek.wordpress.com).
//Portions created by Mar3ek are Copyright (C) 2010
//Mar3ek (http://mar3ek.wordpress.com). All Rights Reserved.

//Contributor(s): ______________________________________.

//Alternatively, the contents of this file may be used under the terms
//of the _____ license (the  "[___] License"), in which case the
//provisions of [______] License are applicable instead of those
//above.  If you wish to allow use of your version of this file only
//under the terms of the [____] License and not to allow others to use
//your version of this file under the MPL, indicate your decision by
//deleting  the provisions above and replace  them with the notice and
//other provisions required by the [___] License.  If you do not delete
//the provisions above, a recipient may use your version of this file
//under either the MPL or the [___] License."

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Xml.Linq;

// total methods: 86
// currently implemented: 86
// current API coverage: ~ 100%

// TODO: Create object-oriented overloads for methods
// TODO: Implement GET /1/related_results/show/:id.{format} when newTwitter becomes available

// Before introducing universal method SendRequest:
//      Lines of code:          1776
//      Cyclomatic complexity:  632
//      Class coupling:         70
//      Maintainability index:  87 (green)

// After intoducing universal SendRequest method to 100% of public methods + complete ReSharper refactorings:
//      Lines of code:          1840
//      Cyclomatic complexity:  851
//      Class coupling:         81
//      Maintainability index:  89 (green)
//
// After implementing newTwitter changes (23. September 2010)
//      Lines of code:          2112
//      Cyclomatic complexity:  1007
//      Class coupling:         89
//      Maintainability index:  91 (green)

namespace TSharp
{
	/// <summary>
	/// Managed wrapper for Twitter API
	/// </summary>
	public partial class Twitter : IDisposable
	{
		///<summary>
		/// Number of allowed requests per hour
		///</summary>
		public const int RateLimitMaxAuth = 350;
		private readonly SecureString _accessToken;
		private readonly SecureString _accessTokenSecret;
		private readonly SecureString _consumerKey;
		private readonly SecureString _consumerSecret;
		private int _rateLimitRemainingAuth = 350;
		private readonly string _userAgent;

		/// <summary>
		/// Remaining request limit
		/// </summary>
		public int RateLimitRemainingAuth
		{
			get { return _rateLimitRemainingAuth; }
		}

		/// <summary>
		/// Creates a new instance of the Twitter class
		/// </summary>
		/// <param name="consumerKey">Application consumer key</param>
		/// <param name="consumerSecret">Application consumer secret</param>
		/// <param name="accessToken">Authorized OAuth token for making requests on users behalf</param>
		/// <param name="accessTokenSecret">Token secret belonging to the specified <i>accessToken</i></param>
		/// <param name="applicationName">Name of the client application (used to construct user agent HTTP header)</param>
		/// <param name="applicationVersion">Version of the client application (used to construct user agent HTTP header)</param>
		/// <exception cref="ArgumentNullException">Argument is null.</exception>
		public Twitter(string consumerKey, string consumerSecret, string accessToken, string accessTokenSecret, string applicationName, Version applicationVersion)
		{
			if (string.IsNullOrEmpty(consumerKey)) throw new ArgumentNullException("consumerKey");
			if (string.IsNullOrEmpty(consumerSecret)) throw new ArgumentNullException("consumerSecret");
			if (string.IsNullOrEmpty(accessToken)) throw new ArgumentNullException("accessToken");
			if (string.IsNullOrEmpty(accessTokenSecret)) throw new ArgumentNullException("accessTokenSecret");

			_consumerKey = new SecureString();
			_consumerSecret = new SecureString();
			_accessToken = new SecureString();
			_accessTokenSecret = new SecureString();

			Array.ForEach(consumerKey.ToArray(), c => _consumerKey.AppendChar(c));
			Array.ForEach(consumerSecret.ToArray(), c => _consumerSecret.AppendChar(c));
			Array.ForEach(accessToken.ToArray(), c => _accessToken.AppendChar(c));
			Array.ForEach(accessTokenSecret.ToArray(), c => _accessTokenSecret.AppendChar(c));

			_userAgent = string.Format("{0} v{1} (TSharp Library v{2})", applicationName, applicationVersion,
									  Assembly.GetExecutingAssembly().GetName().Version);
		
			
		}

		/// <summary>
		/// Generates HTTP header used for authentication with Twitter API
		/// </summary>
		/// <param name="method">Method used to make the request</param>
		/// <param name="endpoint">Endpoint of the request</param>
		/// <param name="param">Parameters of the request</param>
		/// <returns>OAuth authorization header</returns>
		private string GenerateOAuthHeader(string method, string endpoint, params KeyValuePair<string, string>[] param)
		{
			var timeStamp = DateTime.Now.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0));
			timeStamp = timeStamp.Subtract(Globals.TwitterTimeDifference);
			var oAuthParams = new Dictionary<string, string>
								  {
									  {"oauth_consumer_key", DSS(_consumerKey)},
									  {"oauth_nonce", Globals.CreateNonce()},
									  {"oauth_signature_method", "HMAC-SHA1"},
									  {"oauth_timestamp", ((int) timeStamp.TotalSeconds).ToString()},
									  {"oauth_token", DSS(_accessToken)},
									  {"oauth_version", "1.0"},
								  };

			var signatureParts = new List<string>();
			var headerParts = new List<string>();

			foreach (var p in oAuthParams)
			{
				signatureParts.Add(string.Format("{0}={1}", p.Key, p.Value));
				headerParts.Add(string.Format("{0}=\"{1}\"", p.Key, p.Value));
			}

			signatureParts.AddRange(param.Select(p => string.Format("{0}={1}", p.Key, Globals.ProperUrlEscape(p.Value))));

			signatureParts.Sort((p1, p2) => p1.CompareTo(p2));

			var paramString = string.Join("&", signatureParts.ToArray());

			var sigBaseString = string.Format("{0}&{1}&{2}", method, Globals.ProperUrlEscape(endpoint),
												 Globals.ProperUrlEscape(paramString));

			string oAuthSignature;
			using (var sha = new HMACSHA1(Encoding.ASCII.GetBytes(DSS(_consumerSecret) + "&" + DSS(_accessTokenSecret))))
			{
				oAuthSignature = Convert.ToBase64String(sha.ComputeHash(Encoding.ASCII.GetBytes(sigBaseString)));
			}

			headerParts.Add(string.Format("oauth_signature=\"{0}\"", Globals.ProperUrlEscape(oAuthSignature)));
			headerParts.Sort((p1, p2) => p1.CompareTo(p2));

			var oAuthHeader = string.Format("OAuth {0}", string.Join(", ", headerParts.ToArray()));

			return oAuthHeader;
		}

		/// <summary>
		/// Decrypts a secure string
		/// </summary>
		/// <param name="stringToDecode">Secure string to decrypt</param>
		/// <returns>Decrypted string</returns>
		/// <exception cref="ArgumentNullException"><paramref name="stringToDecode" /> is <c>null</c>.</exception>
		private static string DSS(SecureString stringToDecode)
		{
			if (stringToDecode == null) throw new ArgumentNullException("stringToDecode");

			var pointer = Marshal.SecureStringToBSTR(stringToDecode);
			return Marshal.PtrToStringBSTR(pointer);
		}

		private static DateTime ParseTiwtterTime(string time)
		{
			var parts = time.Split(' ');
			var day = int.Parse(parts[2]);
			var month = 0;
			switch (parts[1])
			{
				case "Jan":
					month = 1;
					break;
				case "Feb":
					month = 2;
					break;
				case "Mar":
					month = 3;
					break;
				case "Apr":
					month = 4;
					break;
				case "May":
					month = 5;
					break;
				case "Jun":
					month = 6;
					break;
				case "Jul":
					month = 7;
					break;
				case "Aug":
					month = 8;
					break;
				case "Sep":
					month = 9;
					break;
				case "Oct":
					month = 10;
					break;
				case "Nov":
					month = 11;
					break;
				case "Dec":
					month = 12;
					break;
			}

			var time2 = DateTime.Parse(parts[3]);
			var year = int.Parse(parts[5]);

			var final = new DateTime(year, month, day, time2.Hour, time2.Minute, time2.Second).ToLocalTime();
			return final;
		}

		/// <summary>
		/// Reads response from web request
		/// </summary>
		/// <param name="request">Web request to read</param>
		/// <param name="isRateLimited"></param>
		/// <returns>Response body</returns>
		/// <exception cref="TwitterException">No response from Twitter</exception>
		/// <exception cref="Exception"><c>Any other exception</c>.</exception>
		private string ReadResponse(WebRequest request, bool isRateLimited)
		{
			try
			{
				var response = request.GetResponse();
				//var str = string.Join("\n", (from key in response.Headers.AllKeys select string.Format("{0}: {1}", key, response.Headers[key])).ToArray());

				if (isRateLimited && response != null && response.Headers["X-RateLimit-Remaining"] != null)
					_rateLimitRemainingAuth = int.Parse(response.Headers["X-RateLimit-Remaining"]);
			  
				var result = string.Empty;

				// ReSharper disable AssignNullToNotNullAttribute
				if (response != null)
					using (var sr = new StreamReader(response.GetResponseStream(), Encoding.Default))
					{
						result = sr.ReadToEnd();
					}
				// ReSharper restore AssignNullToNotNullAttribute

				if (string.IsNullOrEmpty(result)) throw new TwitterException("No response from Twitter");

				return result;
			}
			catch (WebException ex)
			{
				Exception exToThrow;
				string result;
				if (ex.Response != null)
				{
					// ReSharper disable AssignNullToNotNullAttribute
					using (var sr = new StreamReader(ex.Response.GetResponseStream()))
					{
						result = sr.ReadToEnd();
					}
					// ReSharper restore AssignNullToNotNullAttribute

					if (result.StartsWith("<?xml"))
					{
						var doc = XDocument.Parse(result);

						// ReSharper disable PossibleNullReferenceException
						exToThrow = new TwitterException(doc.Element("hash").Element("error").Value, doc.Element("hash").Element("request").Value);
						// ReSharper restore PossibleNullReferenceException
					}
					else
					{
						exToThrow = ex.Message.Contains("420")
										? new TooManyConnectionsException()
										: new TwitterException("Unspecified communication error");
					}
				}
				else
				{
					exToThrow =
						new TwitterException(
							"Unable to connect to Twitter servers. Check your internet connection and try again later.");
				}

				throw exToThrow;
			}
		}

		private string SendRequest(RequestMethod requestMethod, string apiEndpoint, bool isRateLimited,
								   params KeyValuePair<string, string>[] parameters)
		{
			string method;

			switch (requestMethod)
			{
				case RequestMethod.Delete:
					method = "DELETE";
					break;
				case RequestMethod.Post:
					method = "POST";
					break;
				case RequestMethod.Head:
					method = "HEAD";
					break;
				default:
					method = "GET";
					break;
			}

			var oAuthHeader = GenerateOAuthHeader(method, apiEndpoint, parameters);

			var signatureParts = parameters.Select(parameter => string.Format("{0}={1}", parameter.Key, Globals.ProperUrlEscape(parameter.Value))).ToList();

			signatureParts.Sort((p1, p2) => p1.CompareTo(p2));

			var paramString = string.Join("&", signatureParts.ToArray());

			HttpWebRequest request;

			if (requestMethod == RequestMethod.Delete || requestMethod == RequestMethod.Get)
			{
				if (signatureParts.Count > 0)
				{
					request = (HttpWebRequest) WebRequest.Create(string.Format("{0}?{1}", apiEndpoint, paramString));
				}
				else
				{
					request = (HttpWebRequest) WebRequest.Create(apiEndpoint);
				}
				request.Method = method;
			}
			else
			{
				request = (HttpWebRequest) WebRequest.Create(apiEndpoint);
				request.ServicePoint.Expect100Continue = false;
				request.ContentType = "application/x-www-form-urlencoded";
				request.Method = method;

				using (var sw = new StreamWriter(request.GetRequestStream()))
				{
					sw.Write(paramString);
				}
			}

			request.UserAgent = _userAgent;
			request.Headers.Add("Authorization", oAuthHeader);
			request.AllowAutoRedirect = false;

			var result = ReadResponse(request, isRateLimited);

			return result;
		}

		#region IDisposable Members

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Releases unmanaged and - optionally - managed resources
		/// </summary>
		/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
		protected void Dispose(bool disposing)
		{
			if (!disposing) return;
			_accessToken.Dispose();
			_accessTokenSecret.Dispose();
			_consumerSecret.Dispose();
			_consumerKey.Dispose();
		}

		#endregion

		private enum RequestMethod
		{
			Delete,
			Get,
			Head,
			Post
		}
	}

	/// <summary>
	/// Represents size of Twitter's profile picture
	/// </summary>
	[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue")]
	public enum ProfileImageSize
	{
		/// <summary>
		/// Image size = 24x24 px
		/// </summary>
		Mini = 1,
		/// <summary>
		/// Image size = 48x48 px
		/// </summary>
		Normal = 2,
		/// <summary>
		/// Image size = 73x73 px
		/// </summary>
		Bigger = 4
	}

	/// <summary>
	/// Represents public visibility of a list
	/// </summary>
	public enum ListMode
	{
		/// <summary>
		/// List is visible to all users
		/// </summary>
		Public,
		/// <summary>
		/// List is visible to its creator only
		/// </summary>
		Private
	}

	/// <summary>
	/// Device delivery status
	/// </summary>
	public enum DeliveryDevice
	{
		/// <summary>
		/// Delivery through SMS
		/// </summary>
		Sms,
		/// <summary>
		/// Disables delivery through SMS
		/// </summary>
		None
	}

	///<summary>
	/// Represents a MIME type of an image
	///</summary>
	public enum ProfileImageMIME
	{
		///<summary>
		/// MIME type of 'image/gif'
		///</summary>
		Gif,
		///<summary>
		/// MIME type of 'image/jpeg'
		///</summary>
		Jpg,
		///<summary>
		/// MIME type of 'image/png'
		///</summary>
		Png
	}

	/// <summary>
	/// Represents type of color used on Twitter accounts
	/// </summary>
	public enum TwitterAccountColor
	{
		///<summary>
		/// Profile background color. 
		///</summary>
		ProfileBackgroundColor,
		/// <summary>
		/// Profile text color. 
		/// </summary>
		ProfileTextColor,
		///<summary>
		/// Profile link color. 
		///</summary>
		ProfileLinkColor,
		///<summary>
		/// Profile sidebar's background color. 
		///</summary>
		ProfileSidebarFillColor,
		/// <summary>
		/// Profile sidebar's border color. 
		/// </summary>
		ProfileSidebarBorderColor
	}

	/// <summary>
	/// Represents a user profile attribute
	/// </summary>
	public enum TwitterAccountAttributeType
	{
		///<summary>
		/// Full name associated with the profile
		///</summary>
		Name,
		/// <summary>
		/// URL associated with the profile
		/// </summary>
		Url,
		///<summary>
		/// The city or country describing where the user of the account is located
		///</summary>
		Location,
		///<summary>
		///  A description of the user owning the account
		///</summary>
		Description,
	}

	/// <summary>
	/// Represents a language to use when retrieving suggestions
	/// </summary>
	public enum TwitterSuggestionLangugage
	{
		/// <summary>
		/// English
		/// </summary>
		English,
		/// <summary>
		/// French
		/// </summary>
		French,
		/// <summary>
		/// German
		/// </summary>
		German,
		/// <summary>
		/// Italian
		/// </summary>
		Italian,
		/// <summary>
		/// Spanish
		/// </summary>
		Spanish
	}

	/// <summary>
	/// Structure representing account totals
	/// </summary>
	public struct TwitterAccountTotals
	{
		/// <summary>
		/// Number of followers
		/// </summary>
		public int Followers;
		/// <summary>
		/// Number of updates (tweets) 
		///  </summary>
		public int Updates;
		/// <summary>
		/// Number of favorites
		/// </summary>
		public int Favorites;
		/// <summary>
		/// Number of friends
		/// </summary>
		public int Friends;
	}

	/// <summary>
	/// Class representing account settings
	/// </summary>
	public class TwitterAccountSettings
	{
		/// <summary>
		/// Describes whether the account is geo enabled
		/// </summary>
		public bool GeoEnabled { get; private set; }

		/// <summary>
		/// Describes whether the account is sleep enabled
		/// </summary>
		public bool SleepEnabled { get; private set; }

		/// <summary>
		/// Start of sleep
		/// </summary>
		public DateTime? SleepStart { get; private set; }

		/// <summary>
		/// End of sleep
		/// </summary>
		public DateTime? SleepEnd { get; private set; }

		/// <summary>
		/// 
		/// </summary>
		/// <param name="geoEnables"></param>
		/// <param name="sleepEnabled"></param>
		/// <param name="sleepStart"></param>
		/// <param name="sleepEnd"></param>
		public TwitterAccountSettings(bool geoEnables, bool sleepEnabled, DateTime? sleepStart, DateTime? sleepEnd)
		{
			GeoEnabled = geoEnables;
			SleepEnabled = sleepEnabled;
			SleepStart = sleepStart;
			SleepEnd = sleepEnd;
		}
	}

	/// <summary>
	/// Class describing the relationship of two users
	/// </summary>
	public enum TwitterUserConnection
	{
		/// <summary>
		/// User A follows user B
		/// </summary>
		Following,
		/// <summary>
		/// User A has sent a follow request to user B
		/// </summary>
		FollowingRequested,
		/// <summary>
		/// User A is followed by user B
		/// </summary>
		FollowedBy,
		/// <summary>
		/// Users have no connection
		/// </summary>
		None
	}
}